package com.gitee.wsl.result.coroutines

import com.gitee.wsl.result.EntryResult
import kotlin.math.nextUp
import kotlin.random.Random
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
import kotlin.time.times
import kotlinx.coroutines.delay

/**
 * Retries a [block] of code with exponential backoff.
 *
 * This function will attempt the operation you give it up to [maxAttempts] times, multiplying the
 * delay between each attempt by [delayFactor], starting from [initialDelay] and not exceeding
 * [maxDelay]. If the operation continues to fail after [maxAttempts] times, it will return the last
 * failure result. If the operation succeeds at any point, it will immediately return the success
 * result.
 *
 * Note: This uses a default exponential backoff strategy with optional jitter. Depending on your
 * use case, you might want to customize the strategy, for example by handling certain kinds of
 * failures differently.
 *
 * @param maxAttempts The maximum number of times to retry the operation.
 * @param initialDelay The delay before the first retry.
 * @param delayFactor The factor by which the delay should increase after each failed attempt.
 * @param maxDelay The maximum delay between retries.
 * @param jitterFactor The maximum factor of jitter to introduce. For example, a value of 0.1 will
 *   introduce up to 10% jitter (both positive and negative).
 * @param onFailure An optional callback for failures, useful for logging.
 * @param shouldRetry An optional callback for indicating whether to retry a failure. This can be
 *   used to short-circuit attempts in the event of some non-retry-able condition.
 * @return The result of the operation if it's successful, or the last failure result if all
 *   attempts fail.
 */
@Suppress("LongParameterList")
tailrec suspend fun <T : Any, E : Any,R:EntryResult<T,E>> retryWithExponentialBackoff(
    maxAttempts: Int = 3,
    initialDelay: Duration = 500.milliseconds,
    delayFactor: Double = 2.0,
    maxDelay: Duration = 10.seconds,
    jitterFactor: Double = 0.25,
    onFailure: ((failure: R) -> Unit)? = null,
    shouldRetry: suspend ((failure: R) -> Boolean) = { true },
    block: suspend () -> R,
): R {
    require(maxAttempts > 0) { "maxAttempts must be greater than 0" }
    val result = block()
    return when  {
        result.isOk -> result
        result.isErr -> {
            val attemptsRemaining = maxAttempts - 1
            onFailure?.invoke(result)
            if (attemptsRemaining == 0 || !shouldRetry(result)) {
                result
            } else {
                val jitter = 1 + Random.nextDouble(-jitterFactor, jitterFactor.nextUp())
                val nextDelay = (initialDelay + initialDelay * jitter).coerceAtMost(maxDelay)
                delay(nextDelay)
                retryWithExponentialBackoff(
                    maxAttempts = attemptsRemaining,
                    initialDelay = delayFactor * initialDelay,
                    delayFactor = delayFactor,
                    maxDelay = maxDelay,
                    jitterFactor = jitterFactor,
                    onFailure = onFailure,
                    shouldRetry = shouldRetry,
                    block = block,
                )
            }
        }
        else -> throw IllegalStateException()
    }
}
